• 需求:

1、随着下拉,view发生位移,松开回弹到原来的位置

2、内部的listview可以正常的上下滑动

3、listview滑到顶部的时候,继续下拉,则是拉动整个外部view,并且松开回弹

这3个需求就会造成事件冲突,那么处理方式就是:listview不是初始状态就是listview自己处理事件,listview还原到了初始状态,外部view处理下拉回弹事件。


需求一个一个的实现,首先第一个下拉回弹

因为里面还要套一个listview,所以我们自定义一个view继承自viewGroup,这里选择的是LinearLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.aidebar.demo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

/**
* @author xzj
* @date 2016/12/6 10:54.
*/

public class MyView extends LinearLayout {
private int startY;
private int moveY;
private int diffY;

public MyView(Context context) {
super(context);
}

public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

}

@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY(); //getY()获取的是按下去的位置在view中的纵坐标
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = y;
break;
case MotionEvent.ACTION_MOVE:
moveY = y;
Log.d("myview", "moveY="+moveY+"|startY="+startY);
//当往下滑动的时候
if ((moveY - startY) > 0) {
//获取到位移距离,并改变布局参数让view动起来
layout(getLeft(), getTop() + (moveY - startY), getRight(), getBottom() + (moveY - startY));
//diffY是用来记录总共的位移数据的,用于在ACTION_UP中还原
//每次走进move都位移了一点点,就重新布局一次,把每次位移的这一点点累加起来
diffY += (moveY - startY);
}
break;
case MotionEvent.ACTION_UP:
// 在ACTION_UP中就不能用moveY-startY了
// 因为每次走到ACTION_MOVE的时候moveY获取的是触摸点离view上边界的距离,在ACTION_MOVE里重新布局后moveY离上边界肯定是固定的,startY在不放手的情况下也是固定的
// 所以如果用moveY-startY的话会是0,就无法回弹了
layout(getLeft(), getTop() - diffY, getRight(), getBottom() - diffY);
diffY = 0;
break;
}
return true;
}
}


OK,实现第二条需求,让内部listview可以滑动,要让子view可以接受到MotionEvent,首先我们自己就不能拦截,那么重写onInterceptTouchEvent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept=false;
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = y;
break;
case MotionEvent.ACTION_MOVE:
moveY = y;
//当往下滑动的时候才拦截,
if ((moveY - startY) > 0) {
isIntercept = true;
}else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return isIntercept;
}

好,拦截方法写完了,但这样的话,所有向下滑动都被我们拦截了,listview就不能向下滑了。

但这是listview的事情,应该由listview来做判断,什么时候拦截什么时候不拦截。

自定义一个MyListView,继承自ListView,并重写onTouchEvent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//down被拦截了,后续所有事件就都收不到了
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (getScrollY() == 0) {
//只有当listview还原了,才将事件交给外层,由外层拦截事件
getParent().requestDisallowInterceptTouchEvent(false);
}else {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onTouchEvent(ev);
}

这么写会发现无效,因为listview不想scrollview,它没有重写getScrollY()方法,直接调用的是父类view的方法,返回值永远是0. 没写也没关系,我们自己写

1
2
3
4
5
6
7
8
9
10
11
//listview没有重写getScrollY方法,我们只能自己写,可是这方法是final的。。所以不要吐槽名字
public int getScrollY1() {
View v = getChildAt(0);
if (v == null) {
return 0;
}
int firstVisiblePosition = getFirstVisiblePosition();
//top的值肯定是<=0的,因为第一个view完全展示的时候top为0,滑上去了就是负数
int top = v.getTop();
return -top + firstVisiblePosition * v.getHeight() ;
}

将上面的getScrollY()替换成getScrollY1()即可。

OK,listview可以正常滑动了,第二条需求完成


大功告成?太年轻了。。

你会发现可以下拉回弹,listview可以上下滑动并且下拉回弹,但是!

你先把listview往上滑一下,松手,然后再下拉试试

会发现在临界状态下,外部的view突然往下移动了一大截。

为什么不松手的情况下,listview可以上下滑动,滑到顶了外部view可以正常下拉并回弹,而先滑动一次listview就不行了呢?

因为我们在外部view的onInterceptTouchEvent()里获取到了startY,所以当listview复原的时候的moveY和这个startY是相等的,外部view就可以正常的下拉回弹。

而先滑动一次listview后,再次点击滑动,获取到的是一个新的startY,而此时你要把listview复原的moveY是大于startY的,所以listview滑到顶的时候再下拉,布局会突然下降一截。

知道原因就好解决了,在listview处理滑动事件的时候,复原的时候,将moveY的值给外部view的startY赋值就行了呗!

怎么传值,请看RxBus工具类,这是用rxjava写的一个取代EventBus的工具。


在MyView中初始化的时候注册一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
RxBus.getInstance().toObservable(Integer.class,"startY")
.subscribe(new RxBusSubscriber<Integer>() {
@Override
public void receive(Integer data) {
startY = data;
}
});
}

在MyListView中加一句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (getScrollY1() == 0) {
//这里加一句,将此时的坐标发给MyView
RxBus.getInstance().send((int)ev.getY(),"startY");
getParent().requestDisallowInterceptTouchEvent(false);
}else {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}

OK,全部搞定,收工~